package org.atc.tools.ant;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.atc.tools.ant.format.*;
import org.atc.tools.ant.parsers.GenericItemParser;
import org.atc.tools.ant.parsers.ItemParser;
import org.atc.tools.ant.writers.CSVItemWriter;
import org.atc.tools.ant.writers.ItemWriter;
import org.atc.tools.ant.writers.PlainTextItemWriter;
import org.atc.tools.ant.writers.XMLItemWriter;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.lang.String.format;

/**
 * A task that outputs TODOs to a file for all source files that contain them
 *
 * @author Alex Collins
 */
public class TODOTask extends MatchingTask {

	private PrintStream outStream;
	private boolean shouldWriteToProperty;

	/**
	 * The delimited string containing the origin and content of TODOs/FIXMEs.
	 * e.g. "My.java: do something,Another.java: code some more"
	 */
	private StringBuffer optBuffer;
	private List<FormattedItem> itemsList;
	private ItemParser itemParser;
	private Map<Format, ItemFormatter> itemFormattersMap;
	private Map<Format, ItemWriter> itemWriterMap;

	public void init() {
		super.init();
	}

	public void execute() throws BuildException {
		super.execute();
		initRequiredFields();
		parseFiles();
		outputFindings();
	}

	/**
	 * For all files in baseDir call processFile(f)
	 */
	private void parseFiles() {
		File baseDir = Configuration.baseDir;
		for (String fName : getDirectoryScanner(baseDir).getIncludedFiles()) {
			File f = new File(format("%s/%s", baseDir.getAbsolutePath(), fName));
			processFile(f);
		}
	}

	private void outputFindings() {
		vlog(format("itemsList before output is %s", itemsList));

		if (shouldWriteToProperty) {
			getProject().setNewProperty(Configuration.property, optBuffer.toString());
		}

		ItemWriter itemWriter = itemWriterMap.get(Configuration.optFormat);
		vlog(format("Writing using itemWriter '%s'", itemWriter));

		if (itemWriter == null) {
			itemWriter = itemWriterMap.get(Format.DEFAULT);
			log(format("Warning: using DEFAULT output format because I couldn't find a writer for '%s'", Configuration.optFormat));
		}

		itemWriter.write(outStream, itemsList);
		outStream.flush();
		outStream.close();
	}

	/**
	 * Scan the file and parse any and all TODO/FIXMEs into the <code>itemsList</code> field.
	 *
	 * @param f
	 */
	private void processFile(File f) {
		try {
			vlog(format("Processing %s", f.getName()));
			final BufferedReader br = new BufferedReader(new FileReader(f));
			String line;
			int lineCounter = 0;
			while ((line = br.readLine()) != null) {
				line = line.trim();
				lineCounter++;
				if (itemParser.containsItem(line)) {
					Item item = newItem(f, line);
					item.setLineNumber(lineCounter);

					FormattedItem fmtItem = formatItem(item);

					vlog(format("Line number %s", lineCounter));

					if (Configuration.regex != null) {
						String replace = Configuration.replace == null ? "" : Configuration.replace;
						vlog(format("Running pattern %s with replace '%s' against '%s'",
								Configuration.regex, replace, fmtItem.getValue()));
						fmtItem.setValue(fmtItem.getValue().replaceAll(Configuration.regex, replace));
						vlog(format("Pattern produced '%s'", fmtItem.getValue()));
					}

					itemsList.add(fmtItem);
					vlog(format("Added item to itemsList: %s", fmtItem));

					//TODO: write only to the list; remove this to the output phase
					if (shouldWriteToProperty) {
						// buffer the val until later
						// Part of a two-stage refactor; output to file is done later; buffering for property is still done here. We'll merge the two concepts later.
						bufferValueForProperty(item);
						vlog("Item buffered");
					}
//					}
				}
			}

		} catch (IOException e) {
			log(format("Error! I couldn't write a TODO to the output file: %s", e.getMessage()));
		}

	}

	private FormattedItem formatItem(Item item) {
		//FIXME: do we risk a null pointer if the optFormat isn't recognisable?
		return itemFormattersMap.get(Configuration.optFormat).format(Configuration.optFormat, item);
	}

	private Item newItem(File f, String line) {
		Item item = itemParser.parse(line);
		item.setOrigin(f.getName());
		return item;
	}

	private void bufferValueForProperty(Item item) {
		String todoStr = item.toString();
		optBuffer.append(todoStr).append(Configuration.delim);
		vlog(format("Buffered %s to property %s with delim %s",
				todoStr, Configuration.property, Configuration.delim));
	}

	/**
	 * Check if we should be writing to a property instead of an output file.
	 *
	 * @return true if the property field is set
	 */
	private boolean shouldWriteToProperty() {
		return Configuration.property != null && !"".equals(Configuration.property);
	}

	private void initRequiredFields() throws BuildException {
		if (Configuration.baseDir == null) {
			log("You didn't specify a directory to recurse, so I'll use the current directory by default. This might not be what you want...");
			Configuration.baseDir = new File(System.getProperty("user.dir"));
		}

		if (Configuration.outFile == null) {
			outStream = System.out;
			log("No output file specified; writing to stdout");
		} else {
			try {
				outStream = new PrintStream(Configuration.outFile);
				log(format("Using file '%s'", Configuration.outFile.getAbsoluteFile()));
			} catch (FileNotFoundException e) {
				log("Error! Couldn't find output file!");
				throw new BuildException(e);
			}
		}

		if (Configuration.regex != null) {
			if (Configuration.replace == null) {
				log("No replace provided for regex; replacing all occurrences with nothing");
			}
		}

		shouldWriteToProperty = shouldWriteToProperty();
		optBuffer = new StringBuffer();
		itemsList = new ArrayList<FormattedItem>();
		itemParser = new GenericItemParser();

		itemFormattersMap = new HashMap<Format, ItemFormatter>();
		//TODO only add those we're generating? Or leave and add support for multiple formats?
		itemFormattersMap.put(Format.DEFAULT, new PlainTextItemFormatter());
		itemFormattersMap.put(Format.CSV, new CSVItemFormatter());
		itemFormattersMap.put(Format.XML, new XMLItemFormatter());

		//TODO move this to a register pattern and code for subclassing?
		itemWriterMap = new HashMap<Format, ItemWriter>();
		itemWriterMap.put(Format.DEFAULT, new PlainTextItemWriter());
		itemWriterMap.put(Format.CSV, new CSVItemWriter());
		itemWriterMap.put(Format.XML, new XMLItemWriter());
	}

	/**
	 * Log <code>msg</code> if this.verbose is true
	 */
	protected final void vlog(String msg) {
		if (Configuration.verbose) log(msg);
	}

	/**
	 * Set the base directory for the source files
	 * Required!
	 */
	public void setDir(File dir) {
		Configuration.baseDir = dir;
	}

	/**
	 * The file to write the TODOs to, or stdout otherwise
	 */
	public void setOutFile(File file) {
		Configuration.outFile = file;
	}

	public void setVerbose(boolean verbose) {
		Configuration.verbose = verbose;
	}

	/**
	 * What format to write the TODOs as. One of CSV, XML or the default (blank):
	 * SourceFilename:TODO_CONTENT
	 */
	public void setFormat(String format) {
		Configuration.optFormat = Format.fromString(format);
	}

	/**
	 * Store the TODOs found in the property identified by <code>property</code>.
	 *
	 * @param property
	 */
	public void setProperty(String property) {
		Configuration.property = property;
	}

	/**
	 * Use the given String as the delimiter for each TODO item.
	 * By default this is set to the system's line separator.
	 */
	public void setDelim(String delim) {
		Configuration.delim = delim;
	}

	public void setPattern(String regex) {
		Configuration.regex = regex;
	}

	public void setReplace(String replace) {
		Configuration.replace = replace;
	}
}
